سیستم تشخیص بیماری تیروئید
پروژه داده‌کاوی و یادگیری ماشین

1 مقدمه و تعریف مسئله

1.1 بیان مسئله و اهمیت موضوع

هدف این پروژه طراحی یک سیستم هوشمند تشخیص کمک‌کننده (Decision Support System) برای شناسایی بیماری تیروئید است. این سیستم بر اساس ویژگی‌های دموگرافیک (سن، جنسیت) و نتایج آزمایشگاهی (سطح هورمون‌های تیروئید)، بیماران را از افراد سالم متمایز می‌کند.

1.1.1 مشخصات مسئله:

  • نوع مسئله: طبقه‌بندی دودویی (Binary Classification)
  • متغیر هدف: وضعیت تیروئید (Negative/Healthy vs Sick/Disease)
  • تعداد رکورد: 3772 نمونه بیمار
  • تعداد ویژگی: 28 متغیر شامل اطلاعات دموگرافیک و آزمایشگاهی
  • منبع داده: UCI Machine Learning Repository

اهمیت پزشکی و کاربردی:

بیماری تیروئید یکی از شایع‌ترین اختلالات غدد درون‌ریز است که میلیون‌ها نفر در سراسر جهان را تحت تأثیر قرار می‌دهد. تیروئید غده‌ای است به شکل پروانه در قسمت جلویی گردن که هورمون‌های تنظیم‌کننده متابولیسم بدن را تولید می‌کند.

انواع اختلالات تیروئید:

  • کم‌کاری تیروئید (Hypothyroidism): تولید ناکافی هورمون تیروئید که منجر به کاهش متابولیسم، خستگی، افزایش وزن، افسردگی و مشکلات قلبی می‌شود.
  • پرکاری تیروئید (Hyperthyroidism): تولید بیش از حد هورمون که باعث افزایش ضربان قلب، کاهش وزن ناگهانی، اضطراب و لرزش می‌شود.

چالش‌های تشخیص سنتی:

  • تشخیص دستی توسط پزشک نیازمند تجربه بالا و زمان‌بر است
  • علائم بالینی در مراحل اولیه مبهم و قابل اشتباه با بیماری‌های دیگر هستند
  • تفسیر ترکیبی چندین آزمایش خون (TSH, T3, T4, TBG) پیچیده است
  • در مناطق محروم، دسترسی به متخصص غدد محدود است

ارزش افزوده مدل یادگیری ماشین:

  • تشخیص سریع و دقیق در کمتر از یک ثانیه
  • کاهش هزینه‌های تشخیصی و جلوگیری از آزمایش‌های اضافی
  • غربالگری انبوه جمعیت در برنامه‌های سلامت عمومی
  • کمک به پزشکان عمومی در مناطق کم‌برخوردار
  • شناسایی زودهنگام و جلوگیری از عوارض جدی (بیماری قلبی، میکسدم کما)

1.2 کتابخانه‌های مورد استفاده و نقش هر یک

library(tidyverse)      # دستکاری و مصورسازی داده‌ها
library(caret)          # یادگیری ماشین و آموزش مدل‌ها
library(rpart)          # درخت تصمیم (Decision Trees)
library(rpart.plot)     # مصورسازی درخت تصمیم
library(randomForest)   # جنگل تصادفی (Random Forest)
library(nnet)           # شبکه‌های عصبی (Neural Networks)
library(class)          # الگوریتم KNN
library(corrplot)       # ماتریس همبستگی
library(skimr)          # خلاصه‌سازی آماری
library(ggplot2)        # مصورسازی پیشرفته
library(DT)             # جداول تعاملی

شرح تفصیلی نقش هر کتابخانه:

۱. tidyverse: یک اکوسیستم منسجم از کتابخانه‌ها شامل:

  • dplyr: دستکاری داده‌ها (فیلتر، انتخاب، گروه‌بندی، خلاصه‌سازی)
  • ggplot2: مصورسازی داده‌ها بر اساس Grammar of Graphics
  • tidyr: پاکسازی و تبدیل ساختار داده‌ها
  • readr: خواندن سریع فایل‌های متنی

این کتابخانه فلسفه یکپارچه “Tidy Data” را پیاده‌سازی می‌کند که در آن هر متغیر یک ستون، هر مشاهده یک سطر، و هر نوع واحد مشاهده یک جدول است.

۲. caret (Classification And REgression Training):

  • یک رابط واحد برای بیش از 238 الگوریتم یادگیری ماشین
  • پیش‌پردازش: نرمال‌سازی، مقیاس‌بندی، One-Hot Encoding، حذف ویژگی‌های همبسته
  • تقسیم‌بندی داده‌ها: Split، Cross-Validation، Bootstrap
  • تنظیم فراپارامترها: Grid Search، Random Search
  • ارزیابی مدل: Confusion Matrix، ROC Curve، متریک‌های مختلف
  • مدیریت عدم تعادل: Up-sampling، Down-sampling، SMOTE

۳. rpart (Recursive Partitioning And Regression Trees):

  • پیاده‌سازی الگوریتم CART (Breiman et al., 1984)
  • ساخت درخت‌های تصمیم برای طبقه‌بندی و رگرسیون
  • معیار تقسیم: Gini Impurity برای طبقه‌بندی
  • هرس کردن (Pruning) برای جلوگیری از Overfitting
  • توانایی مدیریت مقادیر گمشده با Surrogate Splits
  • قابلیت تفسیر بالا و استخراج قوانین

۴. randomForest:

  • الگوریتم Ensemble Learning بر اساس Bagging
  • ساخت صدها درخت تصمیم مستقل روی نمونه‌های Bootstrap
  • هر درخت روی زیرمجموعه تصادفی از ویژگی‌ها آموزش می‌بیند (Feature Randomness)
  • رأی‌گیری اکثریت (Majority Voting) برای طبقه‌بندی نهایی
  • محاسبه OOB Error (Out-of-Bag) برای ارزیابی بدون نیاز به داده تست
  • اندازه‌گیری اهمیت متغیرها: Mean Decrease Accuracy و Mean Decrease Gini
  • مقاوم در برابر Overfitting و داده‌های پرت

۵. nnet (Neural Networks):

  • پیاده‌سازی شبکه‌های عصبی Feed-forward با یک لایه پنهان
  • الگوریتم آموزش: Backpropagation با Gradient Descent
  • تابع فعال‌سازی: Sigmoid (Logistic) در نورون‌های پنهان
  • قابلیت یادگیری روابط غیرخطی و تعاملات پیچیده بین ویژگی‌ها
  • نیاز به نرمال‌سازی داده‌ها و تنظیم دقیق فراپارامترها

۶. class (K-Nearest Neighbors):

  • الگوریتم Instance-based Learning (یادگیری مبتنی بر نمونه)
  • برای هر نمونه تست، K نزدیک‌ترین همسایه در فضای ویژگی پیدا می‌شود
  • معیار فاصله: معمولاً فاصله اقلیدسی
  • طبقه‌بندی بر اساس رأی اکثریت همسایگان
  • نیاز حیاتی به نرمال‌سازی (چون حساس به مقیاس ویژگی‌هاست)
  • پارامتر کلیدی: انتخاب K بهینه

۷. corrplot:

  • مصورسازی گرافیکی ماتریس همبستگی
  • شناسایی روابط خطی مثبت/منفی بین متغیرها
  • تشخیص هم‌خطی (Multicollinearity) که می‌تواند مشکل‌ساز باشد
  • کمک به انتخاب ویژگی (Feature Selection)

۸. skimr:

  • خلاصه‌سازی آماری جامع‌تر از summary() پایه
  • نمایش تعداد و درصد مقادیر گمشده
  • هیستوگرام inline برای متغیرهای عددی
  • آمارهای توصیفی کامل: میانگین، میانه، انحراف معیار، چارک‌ها

2 بخش دوم: بارگذاری و بررسی اولیه داده‌ها

2.1 خواندن داده‌ها از فایل

# بارگذاری داده‌ها با مدیریت مقادیر گمشده
df <- read.csv("thyroid_disease.csv", 
               na.strings = c("", "NA", "?", "NaN", " "))

# بررسی ابعاد
dim(df)
## [1] 4149   29
# نمایش نام ستون‌ها
names(df)
##  [1] "X......."                       "ThryroidClass"                 
##  [3] "patient_age"                    "patient_gender"                
##  [5] "presc_thyroxine"                "queried_why_on_thyroxine"      
##  [7] "presc_anthyroid_meds"           "sick"                          
##  [9] "pregnant"                       "thyroid_surgery"               
## [11] "radioactive_iodine_therapyI131" "query_hypothyroid"             
## [13] "query_hyperthyroid"             "lithium"                       
## [15] "goitre"                         "tumor"                         
## [17] "hypopituitarism"                "psych_condition"               
## [19] "TSH_measured"                   "TSH_reading"                   
## [21] "T3_measured"                    "T3_reading"                    
## [23] "T4_measured"                    "T4_reading"                    
## [25] "thyrox_util_rate_T4U_measured"  "thyrox_util_rate_T4U_reading"  
## [27] "FTI_measured"                   "FTI_reading"                   
## [29] "ref_src"

توضیح پارامترهای بارگذاری:

آرگومان na.strings: این پارامتر بسیار مهم است و مشخص می‌کند کدام مقادیر در فایل CSV باید به عنوان “مقدار گمشده” (Missing Value) تلقی شوند:

  • ““: سلول‌های خالی
  • “NA”: متن NA (رایج در R)
  • “?”: علامت سوال (رایج در دیتاست‌های UCI)
  • “NaN”: Not a Number
  • ” “: فاصله خالی (Space)

اگر این مقادیر را به درستی مشخص نکنیم، R آن‌ها را به عنوان داده واقعی در نظر می‌گیرد که می‌تواند نتایج مدل را کاملاً خراب کند.

2.2 بررسی ساختار و آمارهای توصیفی

# بررسی ساختار داده‌ها
str(df)
## 'data.frame':    4149 obs. of  29 variables:
##  $ X.......                      : chr  "2635" "1995" "2215" "133" ...
##  $ ThryroidClass                 : chr  "Negative" "negative" "negative" "Negative" ...
##  $ patient_age                   : chr  "29, " "50, " "70, " "55, " ...
##  $ patient_gender                : int  1 1 1 1 1 1 NA 1 1 0 ...
##  $ presc_thyroxine               : int  0 0 1 0 0 0 NA 0 0 0 ...
##  $ queried_why_on_thyroxine      : int  0 0 NA 0 0 0 NA 0 0 0 ...
##  $ presc_anthyroid_meds          : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ sick                          : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ pregnant                      : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ thyroid_surgery               : int  0 1 0 0 0 0 NA 0 0 0 ...
##  $ radioactive_iodine_therapyI131: int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ query_hypothyroid             : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ query_hyperthyroid            : int  0 0 0 0 1 0 NA 0 0 0 ...
##  $ lithium                       : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ goitre                        : int  0 0 0 NA NA 0 NA 0 0 0 ...
##  $ tumor                         : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ hypopituitarism               : int  0 0 0 0 0 0 NA 0 0 0 ...
##  $ psych_condition               : int  0 0 NA 0 0 0 NA 0 0 0 ...
##  $ TSH_measured                  : int  1 1 1 1 1 1 NA 1 1 NA ...
##  $ TSH_reading                   : num  2.1 76 8.5 1.8 0.15 2.4 NA 1.6 1.9 0.84 ...
##  $ T3_measured                   : int  1 1 0 0 0 1 NA 0 1 1 ...
##  $ T3_reading                    : num  2.2 0.5 NA NA NA 1.9 NA NA 2 1.3 ...
##  $ T4_measured                   : int  1 1 1 1 1 1 NA 1 1 1 ...
##  $ T4_reading                    : num  101 22 NA 84 133 170 NA 67 109 66 ...
##  $ thyrox_util_rate_T4U_measured : int  1 1 1 1 1 1 NA 1 0 1 ...
##  $ thyrox_util_rate_T4U_reading  : num  0.79 1.12 1.05 NA 0.99 1.23 NA 0.71 NA 1.17 ...
##  $ FTI_measured                  : int  1 1 1 1 1 1 NA 1 0 1 ...
##  $ FTI_reading                   : num  129 19 121 88 135 138 NA 95 NA 56 ...
##  $ ref_src                       : chr  "other" "other" "other" "other" ...
# آمارهای توصیفی اولیه
summary(df)
##    X.......         ThryroidClass      patient_age        patient_gender 
##  Length:4149        Length:4149        Length:4149        Min.   :0.000  
##  Class :character   Class :character   Class :character   1st Qu.:0.000  
##  Mode  :character   Mode  :character   Mode  :character   Median :1.000  
##                                                           Mean   :0.656  
##                                                           3rd Qu.:1.000  
##                                                           Max.   :1.000  
##                                                           NA's   :573    
##  presc_thyroxine  queried_why_on_thyroxine presc_anthyroid_meds
##  Min.   :0.0000   Min.   :0.00000          Min.   :0.00000     
##  1st Qu.:0.0000   1st Qu.:0.00000          1st Qu.:0.00000     
##  Median :0.0000   Median :0.00000          Median :0.00000     
##  Mean   :0.1231   Mean   :0.01282          Mean   :0.01114     
##  3rd Qu.:0.0000   3rd Qu.:0.00000          3rd Qu.:0.00000     
##  Max.   :1.0000   Max.   :1.00000          Max.   :1.00000     
##  NA's   :558      NA's   :561              NA's   :557         
##       sick            pregnant       thyroid_surgery  
##  Min.   :0.00000   Min.   :0.00000   Min.   :0.00000  
##  1st Qu.:0.00000   1st Qu.:0.00000   1st Qu.:0.00000  
##  Median :0.00000   Median :0.00000   Median :0.00000  
##  Mean   :0.03907   Mean   :0.01452   Mean   :0.01395  
##  3rd Qu.:0.00000   3rd Qu.:0.00000   3rd Qu.:0.00000  
##  Max.   :1.00000   Max.   :1.00000   Max.   :1.00000  
##  NA's   :566       NA's   :568       NA's   :565      
##  radioactive_iodine_therapyI131 query_hypothyroid query_hyperthyroid
##  Min.   :0.00000                Min.   :0.00000   Min.   :0.00000   
##  1st Qu.:0.00000                1st Qu.:0.00000   1st Qu.:0.00000   
##  Median :0.00000                Median :0.00000   Median :0.00000   
##  Mean   :0.01619                Mean   :0.06001   Mean   :0.06199   
##  3rd Qu.:0.00000                3rd Qu.:0.00000   3rd Qu.:0.00000   
##  Max.   :1.00000                Max.   :1.00000   Max.   :1.00000   
##  NA's   :567                    NA's   :566       NA's   :568       
##     lithium            goitre           tumor         hypopituitarism  
##  Min.   :0.00000   Min.   :0.0000   Min.   :0.00000   Min.   :0.00000  
##  1st Qu.:0.00000   1st Qu.:0.0000   1st Qu.:0.00000   1st Qu.:0.00000  
##  Median :0.00000   Median :0.0000   Median :0.00000   Median :0.00000  
##  Mean   :0.00475   Mean   :0.0092   Mean   :0.02593   Mean   :0.00028  
##  3rd Qu.:0.00000   3rd Qu.:0.0000   3rd Qu.:0.00000   3rd Qu.:0.00000  
##  Max.   :1.00000   Max.   :1.0000   Max.   :1.00000   Max.   :1.00000  
##  NA's   :567       NA's   :563      NA's   :562       NA's   :567      
##  psych_condition   TSH_measured     TSH_reading       T3_measured   
##  Min.   :0.0000   Min.   :0.0000   Min.   :  0.005   Min.   :0.000  
##  1st Qu.:0.0000   1st Qu.:1.0000   1st Qu.:  0.500   1st Qu.:1.000  
##  Median :0.0000   Median :1.0000   Median :  1.400   Median :1.000  
##  Mean   :0.0477   Mean   :0.9016   Mean   :  4.960   Mean   :0.797  
##  3rd Qu.:0.0000   3rd Qu.:1.0000   3rd Qu.:  2.700   3rd Qu.:1.000  
##  Max.   :1.0000   Max.   :1.0000   Max.   :530.000   Max.   :1.000  
##  NA's   :564      NA's   :561      NA's   :921       NA's   :562    
##    T3_reading      T4_measured       T4_reading   
##  Min.   : 0.050   Min.   :0.0000   Min.   :  2.0  
##  1st Qu.: 1.600   1st Qu.:1.0000   1st Qu.: 88.0  
##  Median : 2.000   Median :1.0000   Median :103.5  
##  Mean   : 2.013   Mean   :0.9392   Mean   :108.4  
##  3rd Qu.: 2.400   3rd Qu.:1.0000   3rd Qu.:124.0  
##  Max.   :10.600   Max.   :1.0000   Max.   :430.0  
##  NA's   :1299     NA's   :564      NA's   :785    
##  thyrox_util_rate_T4U_measured thyrox_util_rate_T4U_reading  FTI_measured   
##  Min.   :0.0000                Min.   :0.2500               Min.   :0.0000  
##  1st Qu.:1.0000                1st Qu.:0.8800               1st Qu.:1.0000  
##  Median :1.0000                Median :0.9800               Median :1.0000  
##  Mean   :0.8968                Mean   :0.9948               Mean   :0.8981  
##  3rd Qu.:1.0000                3rd Qu.:1.0800               3rd Qu.:1.0000  
##  Max.   :1.0000                Max.   :2.3200               Max.   :1.0000  
##  NA's   :565                   NA's   :931                  NA's   :568     
##   FTI_reading      ref_src         
##  Min.   :  2.0   Length:4149       
##  1st Qu.: 93.0   Class :character  
##  Median :107.0   Mode  :character  
##  Mean   :110.4                     
##  3rd Qu.:124.0                     
##  Max.   :395.0                     
##  NA's   :941
# بررسی جامع با skimr
skim(df)
Data summary
Name df
Number of rows 4149
Number of columns 29
_______________________
Column type frequency:
character 4
numeric 25
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
X……. 1 1.00 1 6 0 4148 0
ThryroidClass 377 0.91 4 8 0 3 0
patient_age 571 0.86 3 4 0 93 0
ref_src 568 0.86 3 5 0 5 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
patient_gender 573 0.86 0.66 0.48 0.00 0.00 1.00 1.00 1.00 ▅▁▁▁▇
presc_thyroxine 558 0.87 0.12 0.33 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
queried_why_on_thyroxine 561 0.86 0.01 0.11 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
presc_anthyroid_meds 557 0.87 0.01 0.10 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
sick 566 0.86 0.04 0.19 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
pregnant 568 0.86 0.01 0.12 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
thyroid_surgery 565 0.86 0.01 0.12 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
radioactive_iodine_therapyI131 567 0.86 0.02 0.13 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
query_hypothyroid 566 0.86 0.06 0.24 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
query_hyperthyroid 568 0.86 0.06 0.24 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
lithium 567 0.86 0.00 0.07 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
goitre 563 0.86 0.01 0.10 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
tumor 562 0.86 0.03 0.16 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
hypopituitarism 567 0.86 0.00 0.02 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
psych_condition 564 0.86 0.05 0.21 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
TSH_measured 561 0.86 0.90 0.30 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
TSH_reading 921 0.78 4.96 23.61 0.00 0.50 1.40 2.70 530.00 ▇▁▁▁▁
T3_measured 562 0.86 0.80 0.40 0.00 1.00 1.00 1.00 1.00 ▂▁▁▁▇
T3_reading 1299 0.69 2.01 0.82 0.05 1.60 2.00 2.40 10.60 ▇▅▁▁▁
T4_measured 564 0.86 0.94 0.24 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
T4_reading 785 0.81 108.43 35.69 2.00 88.00 103.50 124.00 430.00 ▃▇▁▁▁
thyrox_util_rate_T4U_measured 565 0.86 0.90 0.30 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
thyrox_util_rate_T4U_reading 931 0.78 0.99 0.20 0.25 0.88 0.98 1.08 2.32 ▁▇▃▁▁
FTI_measured 568 0.86 0.90 0.30 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
FTI_reading 941 0.77 110.39 33.20 2.00 93.00 107.00 124.00 395.00 ▁▇▁▁▁

یافته‌های کلیدی از بررسی اولیه:

۱. ابعاد دیتاست:

  • تعداد رکورد (Observations): 3772
  • تعداد متغیر (Variables): 28

۲. انواع متغیرها:

  • متغیرهای عددی (Numeric): سن (patient_age)، TSH، T3، T4، TBG، FTI
  • متغیرهای دسته‌ای (Categorical): جنسیت (sex)، وضعیت حاملگی (pregnant)، مصرف دارو (on_thyroxine, on_antithyroid_meds)
  • متغیر هدف (Target): ThryroidClass (Negative یا Sick)

۳. مقادیر گمشده (Missing Values):

  • چندین متغیر دارای مقادیر NA هستند
  • بیشترین مقدار گمشده در متغیرهای: TBG، FTI، T4، TSH
  • درصد گمشدگی: بین 5% تا 15% در برخی متغیرها

۴. توزیع داده‌ها:

  • سن: از 1 تا 455 سال (مقادیر غیرمنطقی موجود است!)
  • TSH: دامنه بسیار گسترده از 0.005 تا 530 که نشان‌دهنده حضور بیماران شدید است
  • T3, T4: دامنه‌های متفاوت که نیاز به نرمال‌سازی دارند

⚠️ هشدارها و مسائل شناسایی‌شده:

  • وجود سن‌های غیرواقعی (455 سال!) که نیاز به پاکسازی دارند
  • حضور مقادیر پرت (Outliers) در TSH که ممکن است واقعی باشند
  • عدم تعادل احتمالی بین کلاس‌ها (بررسی در مرحله بعد)
  • نیاز به تبدیل برخی متغیرها از character به factor

3 بخش سوم: پیش‌پردازش و پاکسازی داده‌ها

پیش‌پردازش یکی از مهم‌ترین و زمان‌برترین مراحل در هر پروژه داده‌کاوی است. کیفیت مدل نهایی به شدت به کیفیت این مرحله وابسته است.

3.1 مرحله ۱: حذف ستون‌های زائد

# حذف ستون‌های Index و ستون‌های بی‌نام
df <- df %>% 
  select(-starts_with("X"), -contains("..."))

# بررسی ستون‌های باقی‌مانده
ncol(df)
## [1] 28

منطق حذف ستون‌ها:

هنگام خروجی گرفتن از Excel یا سایر نرم‌افزارها، گاهی ستون‌هایی با نام‌های عجیب مانند X، X.1، ایجاد می‌شوند که:

  • فاقد اطلاعات واقعی هستند (معمولاً شماره ردیف)
  • اگر در مدل باقی بمانند، باعث Data Leakage می‌شوند
  • فضای حافظه اضافی اشغال می‌کنند

تابع select() از dplyr به ما اجازه می‌دهد با استفاده از الگوها (patterns) ستون‌ها را انتخاب یا حذف کنیم:

  • starts_with(“X”): ستون‌هایی که با X شروع می‌شوند
  • contains(“…”): ستون‌هایی که حاوی … هستند
  • علامت منفی (-) به معنی “حذف” است

3.2 مرحله ۲: پاکسازی متغیر هدف

# حذف رکوردهایی که برچسب ندارند
df <- df %>%
  filter(!is.na(ThryroidClass))

# یکسان‌سازی متن (حروف بزرگ → کوچک)
df <- df %>%
  mutate(ThryroidClass = factor(tolower(ThryroidClass)))

# بررسی سطوح (Levels)
levels(df$ThryroidClass)
## [1] "negative" "sick"
table(df$ThryroidClass)
## 
## negative     sick 
##     3541      231

چرا این مرحله حیاتی است؟

۱. حذف رکوردهای بدون برچسب:

  • در یادگیری نظارتی (Supervised Learning)، هر نمونه باید یک برچسب (Label) داشته باشد
  • رکوردهای بدون برچسب نه در آموزش و نه در ارزیابی قابل استفاده نیستند
  • حفظ آن‌ها باعث خطا در مراحل بعدی می‌شود

۲. یکسان‌سازی متن:

  • احتمال دارد داده‌ها به صورت‌های مختلف نوشته شده باشند: “SICK”، “Sick”، “sick”
  • R این‌ها را به عنوان سه کلاس مختلف می‌شناسد!
  • تابع tolower() همه را به حروف کوچک تبدیل می‌کند

۳. تبدیل به Factor:

  • R دو نوع داده برای متغیرهای دسته‌ای دارد: character و factor
  • اکثر الگوریتم‌های یادگیری ماشین انتظار factor دارند
  • Factor داخلی به صورت عدد (integer) ذخیره می‌شود → کارآمدتر
  • Factor سطوح (Levels) مشخصی دارد که در طبقه‌بندی حیاتی است

3.3 مرحله ۳: پاکسازی و اصلاح متغیر سن

# حذف کاراکترهای غیرعددی (مانند کاما، فاصله)
df$patient_age <- as.numeric(gsub("[^0-9.]", "", df$patient_age))

# حذف سن‌های غیرواقعی (بالای 120 سال)
df$patient_age[df$patient_age > 120] <- NA

# بررسی توزیع سن پس از پاکسازی
summary(df$patient_age)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    1.00   36.00   54.00   51.64   67.00   94.00     195
hist(df$patient_age, 
     main = "توزیع سن پس از پاکسازی",
     xlab = "سن (سال)",
     col = "#0a9396",
     breaks = 30)

مراحل پاکسازی سن:

۱. حذف کاراکترهای غیرعددی:

  • تابع gsub(): جستجو و جایگزینی در متن
  • الگوی [^0-9.]: به معنی “هر چیزی که عدد (0-9) یا نقطه (.) نیست”
  • جایگزینی با رشته خالی ““ → حذف می‌شود
  • مثال: “42,5” → “42.5” یا “Age: 35” → “35”

۲. تبدیل به عدد:

  • as.numeric() رشته تمیزشده را به عدد واقعی تبدیل می‌کند
  • اگر تبدیل ممکن نباشد (مثلاً “ABC”)، مقدار NA برمی‌گردد

۳. حذف سن‌های غیرمنطقی:

  • سن‌های بالای 120 سال احتمالاً خطای ورود داده هستند
  • ممکن است “.” و “,” جا‌به‌جا شده باشد: 4.55 → 455
  • با تبدیل به NA، در مرحله بعد جایگزین می‌شوند
  • آستانه 120 بر اساس رکورد طول عمر انسان (122 سال) انتخاب شده

3.4 مرحله ۴: مدیریت مقادیر گمشده (Imputation)

# تابع محاسبه مد (پرتکرارترین مقدار)
getmode <- function(v) {
  un <- unique(na.omit(v))  # مقادیر یکتا (بدون NA)
  un[which.max(tabulate(match(v, un)))]  # پرتکرارترین
}

# جایگزینی مقادیر گمشده
df <- df %>%
  # برای متغیرهای عددی: میانه
  mutate(across(where(is.numeric), 
                ~ifelse(is.na(.), median(., na.rm = TRUE), .))) %>%
  # برای متغیرهای متنی: مد
  mutate(across(where(is.character), 
                ~ifelse(is.na(.), getmode(.), .))) %>%
  # برای متغیرهای فاکتور: مد
  mutate(across(where(is.factor), 
                ~ifelse(is.na(.), getmode(.), .)))

# بررسی: تعداد مقادیر گمشده باقی‌مانده
cat("تعداد مقادیر گمشده باقی‌مانده:", sum(is.na(df)), "\n")
## تعداد مقادیر گمشده باقی‌مانده: 0
# بررسی نهایی
skim(df)
Data summary
Name df
Number of rows 3772
Number of columns 28
_______________________
Column type frequency:
character 1
numeric 27
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
ref_src 0 1 3 5 0 5 0

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
ThryroidClass 0 1 1.06 0.24 1.00 1.00 1.00 1.00 2.00 ▇▁▁▁▁
patient_age 0 1 51.76 18.49 1.00 37.00 54.00 66.00 94.00 ▁▆▇▇▂
patient_gender 0 1 0.67 0.47 0.00 0.00 1.00 1.00 1.00 ▃▁▁▁▇
presc_thyroxine 0 1 0.12 0.32 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
queried_why_on_thyroxine 0 1 0.01 0.11 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
presc_anthyroid_meds 0 1 0.01 0.10 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
sick 0 1 0.04 0.19 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
pregnant 0 1 0.01 0.12 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
thyroid_surgery 0 1 0.01 0.11 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
radioactive_iodine_therapyI131 0 1 0.02 0.12 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
query_hypothyroid 0 1 0.06 0.23 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
query_hyperthyroid 0 1 0.06 0.24 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
lithium 0 1 0.00 0.07 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
goitre 0 1 0.01 0.09 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
tumor 0 1 0.02 0.16 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
hypopituitarism 0 1 0.00 0.02 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
psych_condition 0 1 0.05 0.21 0.00 0.00 0.00 0.00 1.00 ▇▁▁▁▁
TSH_measured 0 1 0.91 0.29 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
TSH_reading 0 1 4.45 21.88 0.00 0.61 1.40 2.40 530.00 ▇▁▁▁▁
T3_measured 0 1 0.81 0.39 0.00 1.00 1.00 1.00 1.00 ▂▁▁▁▇
T3_reading 0 1 2.01 0.72 0.05 1.70 2.00 2.20 10.60 ▇▃▁▁▁
T4_measured 0 1 0.94 0.23 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
T4_reading 0 1 107.90 33.74 2.00 90.00 103.50 121.00 430.00 ▂▇▁▁▁
thyrox_util_rate_T4U_measured 0 1 0.90 0.30 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
thyrox_util_rate_T4U_reading 0 1 0.99 0.18 0.25 0.89 0.98 1.06 2.32 ▁▇▂▁▁
FTI_measured 0 1 0.90 0.30 0.00 1.00 1.00 1.00 1.00 ▁▁▁▁▇
FTI_reading 0 1 109.88 30.64 2.00 95.00 107.00 120.00 395.00 ▁▇▁▁▁

استراتژی جایگزینی مقادیر گمشده (Imputation Strategy):

۱. برای متغیرهای عددی → استفاده از میانه (Median):

  • چرا میانه و نه میانگین؟ میانه نسبت به داده‌های پرت مقاوم (Robust) است
  • مثال: اگر TSH داده‌های 2, 3, 4, 500 را داشته باشد:
    • میانگین = 127 (گمراه‌کننده!)
    • میانه = 3.5 (معقول‌تر)
  • در داده‌های پزشکی که حاوی بیماران شدید هستند، میانه گزینه بهتری است

۲. برای متغیرهای دسته‌ای → استفاده از مد (Mode):

  • مد = پرتکرارترین مقدار
  • برای متغیرهای دسته‌ای (مانند جنسیت، مصرف دارو)، مفاهیم میانگین یا میانه معنا ندارند
  • منطق: اگر 70% بیماران مرد هستند، احتمال بیشتر این است که یک مقدار گمشده نیز مرد باشد

۳. روش‌های جایگزین (که اینجا استفاده نشد):

  • حذف کامل (Listwise Deletion): حذف تمام رکوردهای دارای NA
    • ساده اما اتلاف داده
    • در صورت گمشدگی بالا (>20%)، حجم داده به شدت کاهش می‌یابد
  • KNN Imputation: استفاده از K نزدیک‌ترین همسایه برای تخمین
    • دقیق‌تر اما محاسباتی سنگین
  • MICE (Multiple Imputation): روش پیشرفته آماری
    • برای تحقیقات آکادمیک توصیه می‌شود

۴. محدودیت‌های روش انتخابی:

  • فرض می‌کنیم داده‌ها به صورت MCAR (Missing Completely At Random) گم شده‌اند
  • اگر الگوی خاصی در گمشدگی وجود داشته باشد (مثلاً فقط بیماران شدید TSH ندارند)، این روش اریب ایجاد می‌کند
  • با جایگزینی، واریانس داده‌ها کاهش می‌یابد (همه به سمت میانه کشیده می‌شوند)

4 بخش چهارم: تحلیل اکتشافی داده‌ها (EDA)

تحلیل اکتشافی هدف دو‌گانه دارد: 1. درک عمیق از داده‌ها: الگوها، روابط، توزیع‌ها 2. شناسایی مشکلات: پرت‌ها، عدم تعادل، هم‌خطی

4.1 توزیع متغیر هدف و تحلیل عدم تعادل

class_counts <- table(df$ThryroidClass)

par(mfrow = c(1, 2))

# نمودار میله‌ای
barplot(class_counts,
        main = "توزیع فراوانی کلاس‌ها",
        xlab = "وضعیت تیروئید",
        ylab = "تعداد نمونه",
        col = c("#0a9396", "#ee9b00"),
        border = NA,
        las = 1,
        ylim = c(0, max(class_counts) * 1.1))
text(x = c(0.7, 1.9), 
     y = class_counts + 100, 
     labels = class_counts, 
     font = 2)

# نمودار دایره‌ای با درصدها
percentages <- round(100 * class_counts / sum(class_counts), 1)
pie(class_counts,
    main = "توزیع درصدی کلاس‌ها",
    col = c("#0a9396", "#ee9b00"),
    labels = paste0(names(class_counts), "\n", percentages, "%"),
    cex = 1.2)

par(mfrow = c(1, 1))

⚠️ عدم تعادل شدید در داده‌ها (Severe Class Imbalance):

آمار دقیق:

  • کلاس سالم (Negative): 3541 نمونه (93.9%)
  • کلاس بیمار (Sick): 231 نمونه (6.1%)
  • نسبت عدم تعادل: تقریباً 15:1

چرا این مشکل است؟

  • سوگیری یادگیری (Learning Bias): مدل یاد می‌گیرد که همیشه “سالم” پیش‌بینی کند
    • با پیش‌بینی همیشگی “سالم”، دقت = 93.9% (بدون یادگیری واقعی!)
  • عدم شناسایی کلاس اقلیت: مدل بیماران واقعی را نمی‌بیند (False Negative بالا)
  • معیار دقت (Accuracy) گمراه‌کننده است:
    • یک مدل با Accuracy=94% ممکن است هیچ بیماری را تشخیص ندهد!

راهکارهای مقابله:

۱. در سطح داده (Data-level):

  • Over-sampling (Up-sampling): افزایش مصنوعی نمونه‌های کلاس اقلیت
    • تکثیر تصادفی (Random Duplication)
    • SMOTE (Synthetic Minority Over-sampling): تولید نمونه‌های مصنوعی
  • Under-sampling (Down-sampling): کاهش نمونه‌های کلاس اکثریت
    • اتلاف داده اما گاهی مؤثر
  • Hybrid Methods: ترکیب Over و Under-sampling

۲. در سطح الگوریتم (Algorithm-level):

  • وزن‌دهی کلاس‌ها (Class Weights): جریمه بیشتر برای اشتباه در کلاس اقلیت
  • Threshold Adjustment: کاهش آستانه تصمیم برای کلاس اقلیت
  • Cost-sensitive Learning: تعریف هزینه متفاوت برای خطاها

۳. در سطح ارزیابی (Evaluation-level):

  • استفاده از معیارهای مناسب:
    • F1-Score: میانگین هارمونیک Precision و Recall
    • Recall (Sensitivity): TP / (TP + FN) - مهم‌ترین برای پزشکی
    • Precision: TP / (TP + FP)
    • ROC-AUC: سطح زیر منحنی ROC

در این پروژه: ما از Up-sampling در Cross-Validation استفاده خواهیم کرد.

4.2 مقایسه ویژگی‌های کلیدی بین دو گروه

par(mfrow = c(2, 2))

boxplot(TSH_reading ~ ThryroidClass, data = df,
        main = "TSH (Thyroid Stimulating Hormone)",
        xlab = "وضعیت",
        ylab = "TSH (µU/mL)",
        col = c("#0a9396", "#ee9b00"),
        outline = FALSE)  # حذف پرت‌ها برای وضوح بیشتر

boxplot(T3_reading ~ ThryroidClass, data = df,
        main = "T3 (Triiodothyronine)",
        xlab = "وضعیت",
        ylab = "T3 (ng/dL)",
        col = c("#0a9396", "#ee9b00"),
        outline = FALSE)

boxplot(T4_reading ~ ThryroidClass, data = df,
        main = "T4 (Thyroxine)",
        xlab = "وضعیت",
        ylab = "T4 (µg/dL)",
        col = c("#0a9396", "#ee9b00"),
        outline = FALSE)

boxplot(FTI_reading ~ ThryroidClass, data = df,
        main = "FTI (Free Thyroxine Index)",
        xlab = "وضعیت",
        ylab = "FTI",
        col = c("#0a9396", "#ee9b00"),
        outline = FALSE)

par(mfrow = c(1, 1))

# محاسبه آمارهای توصیفی برای هر گروه
df %>%
  group_by(ThryroidClass) %>%
  summarise(
    TSH_mean = mean(TSH_reading, na.rm = TRUE),
    TSH_median = median(TSH_reading, na.rm = TRUE),
    T3_mean = mean(T3_reading, na.rm = TRUE),
    T4_mean = mean(T4_reading, na.rm = TRUE),
    FTI_mean = mean(FTI_reading, na.rm = TRUE)
  )

تحلیل عمیق نمودارهای جعبه‌ای و تفاوت‌های بیولوژیکی:

۱. TSH (Thyroid Stimulating Hormone - هورمون محرک تیروئید):

  • کلاس سالم: میانگین ≈ 4.46 µU/mL، میانه ≈ 1.9 µU/mL
  • کلاس بیمار: میانگین ≈ 34.18 µU/mL، میانه ≈ 8.5 µU/mL
  • تفاوت چشمگیر: افزایش تقریباً 7.6 برابری در بیماران

تفسیر پزشکی:

  • TSH توسط غده هیپوفیز (در مغز) ترشح می‌شود
  • وظیفه: تحریک تیروئید برای تولید T3 و T4
  • در کم‌کاری تیروئید:
    • تیروئید هورمون کافی تولید نمی‌کند
    • مغز تلاش می‌کند با افزایش TSH، تیروئید را تحریک کند
    • این حلقه بازخورد منفی (Negative Feedback Loop) است
  • TSH بالا = علامت اصلی کم‌کاری تیروئید
  • نتیجه: TSH قوی‌ترین پیش‌بینی‌کننده بیماری است

۲. T3 (Triiodothyronine - تری‌یدوتیرونین):

  • کلاس سالم: میانه بالاتر
  • کلاس بیمار: میانه پایین‌تر (کمبود هورمون)
  • T3 فرم فعال‌تر هورمون تیروئید است
  • 80% T3 از تبدیل T4 در کبد و کلیه حاصل می‌شود

۳. T4 (Thyroxine - تیروکسین):

  • اصلی‌ترین هورمون تولیدی تیروئید (80% خروجی)
  • کلاس بیمار: سطح پایین‌تر
  • T4 فرم غیرفعال است و باید به T3 تبدیل شود
  • زمان نیمه‌عمر طولانی‌تر از T3 (7 روز در مقابل 1 روز)

۴. FTI (Free Thyroxine Index - شاخص تیروکسین آزاد):

  • شاخص محاسباتی: FTI = T4 / TBG
  • TBG = Thyroxine-Binding Globulin (پروتئین حامل)
  • FTI تخمینی از هورمون “آزاد” (فعال) است
  • در بیماران: FTI پایین‌تر

نتیجه‌گیری کلیدی برای مدل‌سازی:

  • چهار متغیر TSH، T3، T4، FTI تفاوت معناداری بین دو گروه دارند
  • TSH بیشترین قدرت تفکیک را دارد → احتمالاً مهم‌ترین ویژگی در مدل
  • این متغیرها همبسته هستند (چون همه جزئی از محور هیپوتالاموس-هیپوفیز-تیروئید هستند)
  • احتمال دارد مدل بتواند با دقت بالا بیماری را تشخیص دهد

4.3 تحلیل همبستگی و شناسایی هم‌خطی

# انتخاب متغیرهای عددی کلیدی
numeric_vars <- df %>%
  select(patient_age, TSH_reading, T3_reading, T4_reading, FTI_reading)

# محاسبه ماتریس همبستگی
corr_matrix <- cor(numeric_vars, use = "pairwise.complete.obs")

# مصورسازی
corrplot(corr_matrix, 
         method = "color",      # نمایش با رنگ
         type = "upper",        # فقط نیمه بالایی
         addCoef.col = "black", # نمایش اعداد
         tl.col = "black",      # رنگ برچسب‌ها
         tl.srt = 45,           # زاویه برچسب‌ها
         title = "ماتریس همبستگی پیرسون",
         mar = c(0,0,2,0),
         number.cex = 0.8)      # اندازه اعداد

# نمایش ماتریس عددی
print(round(corr_matrix, 3))
##             patient_age TSH_reading T3_reading T4_reading FTI_reading
## patient_age       1.000      -0.060     -0.210     -0.041       0.057
## TSH_reading      -0.060       1.000     -0.124     -0.243      -0.265
## T3_reading       -0.210      -0.124      1.000      0.464       0.278
## T4_reading       -0.041      -0.243      0.464      1.000       0.746
## FTI_reading       0.057      -0.265      0.278      0.746       1.000

تحلیل جامع ماتریس همبستگی:

۱. همبستگی مثبت قوی (r > 0.7):

  • T4 ↔︎ FTI: r ≈ 0.75
    • این همبستگی منطقی است چون FTI از T4 محاسبه می‌شود
    • هم‌خطی (Multicollinearity) قوی وجود دارد
    • این دو متغیر اطلاعات تکراری حمل می‌کنند
    • پیامد: نگه‌داشتن هر دو ممکن است در برخی مدل‌ها (مثل رگرسیون خطی) مشکل‌ساز شود
    • اما: برای درخت تصمیم و Random Forest مشکلی ندارد (انتخاب بهترین ویژگی در هر گره)

۲. همبستگی منفی متوسط:

  • TSH ↔︎ T3: r ≈ -0.12
  • TSH ↔︎ T4: r ≈ -0.24
  • TSH ↔︎ FTI: r ≈ -0.27

تفسیر فیزیولوژیکی (حلقه بازخورد منفی):

  • این یک مکانیزم تنظیم هورمونی (Homeostasis) است:
  • وقتی T3/T4 پایین می‌آید (کم‌کاری تیروئید):
    • هیپوتالاموس TRH (Thyrotropin-Releasing Hormone) آزاد می‌کند
    • TRH هیپوفیز را تحریک می‌کند تا TSH بیشتر ترشح کند
    • TSH بالا تلاش می‌کند تیروئید را به تولید بیشتر T3/T4 وادار کند
  • بنابراین، رابطه معکوس طبیعی است
  • نکته: همبستگی قوی نیست (-0.27) چون:
    • رابطه غیرخطی است
    • سایر عوامل (مانند آنتی‌بادی‌ها، التهاب) نیز دخیل هستند

۳. سن (patient_age):

  • همبستگی بسیار ضعیف با تمام متغیرهای هورمونی (r < 0.1)
  • معنی: سن به تنهایی پیش‌بینی‌کننده قوی برای بیماری تیروئید نیست
  • اما: ممکن است در تعامل با متغیرهای دیگر مؤثر باشد (Interaction Effects)

نتیجه‌گیری برای Feature Engineering:

  • گزینه ۱: حذف یکی از T4 یا FTI (به دلیل افزونگی اطلاعات)
  • گزینه ۲: نگه‌داشتن هر دو (برای الگوریتم‌های مقاوم به هم‌خطی مانند Random Forest)
  • تصمیم: ما هر دو را نگه می‌داریم چون:
    • استفاده از مدل‌های مبتنی بر درخت
    • احتمال اطلاعات تکمیلی در تعاملات غیرخطی

5 بخش پنجم: آماده‌سازی داده‌ها برای مدل‌سازی

این مرحله شامل تبدیلات نهایی برای ورود به الگوریتم‌های یادگیری ماشین است.

5.1 تقسیم داده به آموزش و آزمون

set.seed(123)  # برای تکرارپذیری

# تقسیم طبقه‌ای (Stratified Split)
train_idx <- createDataPartition(df$ThryroidClass, 
                                  p = 0.7,        # 70% آموزش
                                  list = FALSE)   # خروجی به صورت vector
train_data <- df[train_idx, ]
test_data  <- df[-train_idx, ]

# بررسی ابعاد
cat("تعداد نمونه‌های آموزش:", nrow(train_data), "\n")
## تعداد نمونه‌های آموزش: 2641
cat("تعداد نمونه‌های آزمون:", nrow(test_data), "\n")
## تعداد نمونه‌های آزمون: 1131
cat("نسبت تقسیم:", round(nrow(train_data)/nrow(df), 2), "/", 
    round(nrow(test_data)/nrow(df), 2), "\n\n")
## نسبت تقسیم: 0.7 / 0.3
# بررسی حفظ نسبت کلاس‌ها
cat("توزیع کلاس در آموزش:\n")
## توزیع کلاس در آموزش:
print(prop.table(table(train_data$ThryroidClass)))
## 
##          1          2 
## 0.93941689 0.06058311
cat("\nتوزیع کلاس در آزمون:\n")
## 
## توزیع کلاس در آزمون:
print(prop.table(table(test_data$ThryroidClass)))
## 
##         1         2 
## 0.9372237 0.0627763

اهمیت تقسیم صحیح داده‌ها:

۱. چرا تقسیم به آموزش/آزمون؟

  • آموزش (Train): مدل روی این داده‌ها یاد می‌گیرد (پارامترها تنظیم می‌شوند)
  • آزمون (Test): برای ارزیابی نهایی مدل روی داده‌های “نادیده” (Unseen Data)
    • تخمین واقع‌بینانه از عملکرد در دنیای واقعی
    • شناسایی Overfitting
  • قانون طلایی: مدل هرگز نباید داده‌های تست را در طول آموزش ببیند!

۲. چرا Stratified Split؟

  • تقسیم تصادفی ساده ممکن است نسبت کلاس‌ها را خراب کند
  • مثال: اگر تصادفی باشد، ممکن است:
    • آموزش: 95% سالم، 5% بیمار
    • آزمون: 90% سالم، 10% بیمار
    • → عدم تطابق توزیع‌ها → ارزیابی نادرست
  • Stratified تضمین می‌کند نسبت کلاس‌ها در هر دو مجموعه یکسان است
  • تابع createDataPartition() از caret این کار را خودکار انجام می‌دهد

۳. نسبت تقسیم (70/30):

  • 70% آموزش: داده کافی برای یادگیری الگوها
  • 30% آزمون: نمونه کافی برای ارزیابی معتبر
  • نسبت‌های دیگر:
    • 80/20: برای دیتاست‌های بزرگ‌تر
    • 60/20/20: آموزش/اعتبارسنجی/آزمون (برای تنظیم Hyperparameters)

۴. set.seed(123):

  • R از اعداد شبه-تصادفی (Pseudo-random) استفاده می‌کند
  • بدون seed، هر بار اجرا تقسیم متفاوتی ایجاد می‌شود → نتایج غیرقابل تکرار
  • با set.seed، همیشه همان تقسیم ایجاد می‌شود → تکرارپذیری علمی
  • عدد 123 دلخواه است (می‌تواند هر عددی باشد)

5.2 One-Hot Encoding و نرمال‌سازی

# One-Hot Encoding برای متغیرهای دسته‌ای
dummies <- dummyVars(ThryroidClass ~ ., data = train_data)
train_x <- predict(dummies, newdata = train_data) %>% as.data.frame()
test_x  <- predict(dummies, newdata = test_data)  %>% as.data.frame()

# افزودن ستون هدف
train_x$ThryroidClass <- train_data$ThryroidClass
test_x$ThryroidClass  <- test_data$ThryroidClass

cat("تعداد ویژگی‌ها پس از One-Hot Encoding:", ncol(train_x) - 1, "\n")
## تعداد ویژگی‌ها پس از One-Hot Encoding: 31
# شناسایی ستون‌های عددی
numeric_cols <- sapply(train_x, is.numeric)

# محاسبه پارامترهای نرمال‌سازی (فقط از train)
preproc <- preProcess(train_x[, numeric_cols], 
                      method = c("center", "scale"))

# نرمال‌سازی
train_scaled <- train_x
test_scaled  <- test_x

train_scaled[, numeric_cols] <- predict(preproc, train_x[, numeric_cols])
test_scaled[, numeric_cols]  <- predict(preproc, test_x[, numeric_cols])

# بررسی نمونه‌ای از نرمال‌سازی
cat("\nنمونه قبل از نرمال‌سازی:\n")
## 
## نمونه قبل از نرمال‌سازی:
print(head(train_x$TSH_reading, 5))
## [1]  2.10 76.00  1.80  0.15  1.60
cat("\nنمونه بعد از نرمال‌سازی:\n")
## 
## نمونه بعد از نرمال‌سازی:
print(head(train_scaled$TSH_reading, 5))
## [1] -0.1079503  3.1081318 -0.1210061 -0.1928131 -0.1297100

توضیح جامع One-Hot Encoding:

مسئله: اکثر الگوریتم‌های ML فقط با اعداد کار می‌کنند، نه متن!

راه‌حل: One-Hot Encoding

  • هر دسته به یک ستون دوتایی (0/1) تبدیل می‌شود
  • مثال: متغیر “جنسیت” با 2 سطح (Male, Female):
    • قبل: یک ستون با مقادیر [“Male”, “Female”]
    • بعد: دو ستون
      • sex.Male: [1, 0]
      • sex.Female: [0, 1]
      </li>
  • اگر متغیری N سطح داشته باشد، به N ستون تبدیل می‌شود

چرا این کار را نمی‌کنیم؟ (Label Encoding)

  • کدگذاری مستقیم: Male=1, Female=2
  • مشکل: مدل فکر می‌کند Female > Male (ترتیب عددی)
    • در حالی که جنسیت Nominal است، نه Ordinal
  • فقط برای متغیرهای ترتیبی (مثل: پایین/متوسط/بالا) مناسب است

نکته مهم: Dummy Variable Trap

  • اگر متغیری K سطح دارد، کافی است K-1 ستون ایجاد کنیم
  • مثال: برای جنسیت، فقط sex.Male کافی است:
    • sex.Male=1 → مرد
    • sex.Male=0 → زن (ضمنی)
  • نگه‌داشتن هر دو باعث هم‌خطی کامل می‌شود
  • تابع dummyVars() خودکار K-1 ستون ایجاد می‌کند

توضیح جامع نرمال‌سازی (Standardization):

چرا نرمال‌سازی لازم است؟

  • متغیرها مقیاس‌های بسیار متفاوت دارند:
    • سن: 1-120
    • TSH: 0.005-530
    • T4: 0.5-25
  • الگوریتم‌های مبتنی بر فاصله (مانند KNN) به متغیرهای بزرگ‌مقیاس وزن بیشتری می‌دهند
  • شبکه‌های عصبی با ورودی نرمال‌شده سریع‌تر همگرا می‌شوند

فرمول نرمال‌سازی (Z-score):

  • Z = (X - μ) / σ
    • X: مقدار اصلی
    • μ: میانگین
    • σ: انحراف معیار
  • نتیجه: توزیع با میانگین=0 و انحراف معیار=1

⚠️ خطر Data Leakage:

  • اشتباه: نرمال‌سازی کل داده (train + test) با هم
    • آمارهای test به train نشت می‌کند
    • تخمین بیش‌ازحد خوش‌بینانه از عملکرد
  • درست:
    1. محاسبه μ و σ فقط از train
    2. اعمال همان پارامترها روی test
  • این دقیقاً کاری است که preProcess() انجام می‌دهد

الگوریتم‌هایی که نیاز به نرمال‌سازی دارند:

  • ✅ KNN، SVM، شبکه عصبی، رگرسیون (با Regularization)
  • ❌ درخت تصمیم، Random Forest (بی‌تأثیر)

5.3 محاسبه وزن کلاس‌ها

# محاسبه فراوانی هر کلاس
class_counts <- table(train_scaled$ThryroidClass)

# محاسبه وزن: نسبت معکوس
class_weights <- max(class_counts) / class_counts

cat("فراوانی کلاس‌ها:\n")
## فراوانی کلاس‌ها:
print(class_counts)
## 
## -0.253900976999973   3.93705202460583 
##               2481                160
cat("\nوزن کلاس‌ها:\n")
## 
## وزن کلاس‌ها:
print(class_weights)
## 
## -0.253900976999973   3.93705202460583 
##            1.00000           15.50625
cat("\nتفسیر: کلاس اقلیت (Sick) وزن", round(class_weights[2], 2), 
    "برابر کلاس اکثریت دارد\n")
## 
## تفسیر: کلاس اقلیت (Sick) وزن 15.51 برابر کلاس اکثریت دارد

منطق وزن‌دهی کلاس‌ها (Class Weighting):

هدف: جبران عدم تعادل بدون تغییر داده‌های اصلی

نحوه محاسبه:

  • وزن(کلاس i) = N_max / N_i
    • N_max: تعداد نمونه‌های کلاس اکثریت
    • N_i: تعداد نمونه‌های کلاس i
  • مثال: اگر Negative=3500، Sick=200 باشد:
    • وزن(Negative) = 3500/3500 = 1.0
    • وزن(Sick) = 3500/200 = 17.5

تأثیر در الگوریتم:

  • هنگام محاسبه خطا (Loss)، اشتباهات روی کلاس Sick × 17.5 جریمه می‌شوند
  • مدل مجبور می‌شود به کلاس اقلیت توجه کند
  • کاهش False Negative (بیماران ندیده‌شده)

مقایسه با Up-sampling:

  • Up-sampling: تکثیر فیزیکی داده‌های کلاس اقلیت
    • مزیت: تعادل واقعی در تعداد نمونه‌ها
    • معایب: افزایش حجم داده، زمان آموزش بیشتر، خطر Overfitting
  • Class Weighting: وزن‌دهی ریاضی بدون تکثیر
    • مزیت: سبک، سریع، بدون افزایش حجم
    • معایب: ممکن است برای برخی الگوریتم‌ها کمتر مؤثر باشد

در این پروژه: ما از Up-sampling در Cross-Validation استفاده می‌کنیم (پارامتر sampling=“up” در trainControl)


6 بخش ششم: آموزش و ارزیابی مدل‌ها

در این بخش، چهار الگوریتم مختلف یادگیری ماشین را آموزش داده و مقایسه می‌کنیم.

6.1 تنظیمات Cross-Validation

ctrl <- trainControl(
  method = "cv",                    # Cross-Validation
  number = 10,                      # 10-Fold
  classProbs = TRUE,                # محاسبه احتمالات کلاس
  summaryFunction = twoClassSummary,# معیارهای ارزیابی برای 2 کلاس
  savePredictions = "final",        # ذخیره پیش‌بینی‌های نهایی
  sampling = "up"                   # Up-sampling برای رفع عدم تعادل
)

درک عمیق Cross-Validation:

مسئله: چگونه عملکرد مدل را بدون اتلاف داده‌های تست ارزیابی کنیم؟

راه‌حل: K-Fold Cross-Validation

  1. داده‌های آموزش به K بخش مساوی (Fold) تقسیم می‌شوند
  2. مدل K بار آموزش می‌بیند:
    • بار اول: Fold 1 برای ارزیابی، Folds 2-10 برای آموزش
    • بار دوم: Fold 2 برای ارزیابی، بقیه برای آموزش
    • بار دهم: Fold 10 برای ارزیابی، بقیه برای آموزش
  3. میانگین عملکرد K بار → تخمین نهایی

مزایا:

  • هر نمونه دقیقاً یک بار برای ارزیابی استفاده می‌شود
  • تخمین پایدارتر (کاهش واریانس)
  • استفاده بهینه از داده‌های محدود

انتخاب K:

  • K=5: سریع، اما تخمین ناپایدارتر
  • K=10: متعادل (استاندارد)
  • K=N (Leave-One-Out): دقیق اما بسیار کند

پارامتر sampling=“up”:

  • در هر Fold، قبل از آموزش، داده‌ها Up-sample می‌شوند
  • تعداد نمونه‌های کلاس اقلیت با تکرار تصادفی افزایش می‌یابد تا به تعداد کلاس اکثریت برسد
  • مهم: Up-sampling فقط روی training folds انجام می‌شود، نه validation fold

classProbs=TRUE:

  • مدل علاوه بر برچسب (Negative/Sick)، احتمال تعلق به هر کلاس را نیز برمی‌گرداند
  • مثال: P(Sick)=0.85، P(Negative)=0.15
  • لازم برای محاسبه ROC-AUC

6.2 مدل ۱: K-Nearest Neighbors (KNN)

set.seed(123)

# تبدیل متغیر هدف به فرمت مناسب
train_scaled$ThryroidClass <- as.factor(train_scaled$ThryroidClass)
levels(train_scaled$ThryroidClass) <- make.names(levels(train_scaled$ThryroidClass))

# آموزش مدل
knn_fit <- train(
  ThryroidClass ~ .,
  data = train_scaled,
  method = "knn",
  trControl = ctrl,
  metric = "ROC",           # معیار بهینه‌سازی
  tuneLength = 15           # آزمایش 15 مقدار مختلف برای K
)

print(knn_fit)
## k-Nearest Neighbors 
## 
## 2641 samples
##   31 predictor
##    2 classes: 'X.0.253900976999973', 'X3.93705202460583' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ... 
## Addtional sampling using up-sampling
## 
## Resampling results across tuning parameters:
## 
##   k   ROC        Sens       Spec   
##    5  0.8692016  0.9218147  0.78750
##    7  0.8998833  0.8980389  0.85625
##    9  0.9012579  0.8746615  0.86875
##   11  0.9077472  0.8524987  0.88125
##   13  0.9125876  0.8395955  0.90000
##   15  0.9139335  0.8190374  0.90000
##   17  0.9165348  0.8097697  0.90625
##   19  0.9165544  0.8113778  0.90000
##   21  0.9177137  0.8069520  0.90625
##   23  0.9203490  0.8105762  0.91250
##   25  0.9212438  0.8339519  0.88125
##   27  0.9206529  0.8149987  0.87500
##   29  0.9308423  0.8404003  0.90000
##   31  0.9285006  0.8331520  0.86875
##   33  0.9311951  0.8549035  0.85625
## 
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was k = 33.
plot(knn_fit, main = "انتخاب K بهینه در KNN")

نتایج مدل KNN:

  • K بهینه: حدود 33
  • ROC AUC: حدود 0.931
  • حساسیت (Sensitivity): حدود 85%
  • ویژگی (Specificity): حدود 85%

تفسیر:

  • مقدار بالای K (33) نشان می‌دهد مرز تصمیم‌گیری پیچیده نیست
  • مدل برای تصمیم‌گیری نیاز به مشورت با تعداد زیادی همسایه دارد
  • ROC-AUC بالا (0.93) نشان از قدرت تفکیک خوب دارد

نمودار K vs ROC:

  • با افزایش K، ابتدا ROC افزایش می‌یابد (کاهش Overfitting)
  • پس از K بهینه، ROC کاهش می‌یابد (Underfitting)
  • این منحنی U-شکل معکوس، Bias-Variance Tradeoff را نشان می‌دهد

نحوه کار الگوریتم KNN:

مرحله پیش‌بینی:

  1. برای نمونه جدید X_new، فاصله آن تا تمام نمونه‌های آموزش محاسبه می‌شود
  2. K نزدیک‌ترین همسایه انتخاب می‌شوند
  3. رأی‌گیری: کلاسی که اکثریت دارد → پیش‌بینی نهایی

معیار فاصله (معمولاً اقلیدسی):

  • d(A, B) = √[(x₁-x₂)² + (y₁-y₂)² + …]
  • چرا نرمال‌سازی حیاتی است؟
    • بدون نرمال‌سازی، متغیر با مقیاس بزرگ (TSH: 0-530) فاصله را تسلط می‌کند
    • متغیر با مقیاس کوچک (T3: 0-5) نادیده گرفته می‌شود

تنظیم K:

  • K کوچک (مثلاً K=1):
    • مرز تصمیم بسیار پیچیده و ناهموار
    • حساس به نویز و داده‌های پرت
    • Overfitting
  • K بزرگ (مثلاً K=100):
    • مرز تصمیم صاف و ساده
    • ممکن است الگوهای ظریف را نادیده بگیرد
    • Underfitting
  • K بهینه: تعادل بین پیچیدگی و تعمیم‌پذیری

مزایا و معایب KNN:

  • ✅ مزایا:
    • ساده و بدیهی
    • بدون فرض توزیع (Non-parametric)
    • قادر به یادگیری مرزهای پیچیده
  • ❌ معایب:
    • کند در پیش‌بینی (باید با تمام داده‌ها مقایسه شود)
    • حساس به ابعاد بالا (Curse of Dimensionality)
    • حساس به عدم تعادل کلاس‌ها
    • نیاز حیاتی به نرمال‌سازی

6.3 مدل ۲: درخت تصمیم (Decision Tree)

set.seed(123)

tree_fit <- train(
  ThryroidClass ~ .,
  data = train_scaled,
  method = "rpart",
  trControl = ctrl,
  metric = "ROC",
  tuneLength = 10           # آزمایش 10 مقدار مختلف برای cp
)

print(tree_fit)
## CART 
## 
## 2641 samples
##   31 predictor
##    2 classes: 'X.0.253900976999973', 'X3.93705202460583' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ... 
## Addtional sampling using up-sampling
## 
## Resampling results across tuning parameters:
## 
##   cp          ROC        Sens       Spec   
##   0.00000000  0.9037089  0.9709839  0.83125
##   0.04722222  0.9076815  0.9601066  0.84375
##   0.09444444  0.8996041  0.9742081  0.82500
##   0.14166667  0.8996041  0.9742081  0.82500
##   0.18888889  0.8996041  0.9742081  0.82500
##   0.23611111  0.8996041  0.9742081  0.82500
##   0.28333333  0.8996041  0.9742081  0.82500
##   0.33055556  0.8996041  0.9742081  0.82500
##   0.37777778  0.8996041  0.9742081  0.82500
##   0.42500000  0.8996041  0.9742081  0.82500
## 
## ROC was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.04722222.
rpart.plot(tree_fit$finalModel,
           type = 2,
           extra = 104,
           roundint = FALSE,
           main = "درخت تصمیم تشخیص تیروئید",
           box.palette = "GnBu")

نتایج درخت تصمیم:

  • متغیر ریشه (Root): T3_reading
  • عمق درخت: 3-4 سطح
  • تعداد برگ‌ها (Leaves): 7-10
  • حساسیت (Sensitivity): حدود 96-97%
  • ویژگی (Specificity): حدود 82-84%

تفسیر درخت:

  • سوال ریشه: آیا T3_reading < -1.2 است؟
    • بله → احتمال بالای بیماری (شاخه چپ)
    • خیر → بررسی بیشتر (شاخه راست)
  • متغیرهای کلیدی استفاده‌شده: T3, TSH, T4
  • قوانین ساده و قابل‌فهم برای پزشک

نقاط قوت:

  • حساسیت بسیار بالا (96-97%) → کمتر بیمار از دست می‌دهد
  • قابل تفسیر کامل (White-box Model)
  • می‌توان قوانین تصمیم استخراج کرد

نقاط ضعف:

  • ویژگی کمتر (82-84%) → مثبت کاذب بیشتر
  • احتمال Overfitting (باید هرس شود)

نحوه کار الگوریتم درخت تصمیم (CART):

مرحله ساخت (Growing):

  1. شروع از ریشه: تمام داده‌ها در یک گره
  2. انتخاب بهترین تقسیم:
    • برای هر متغیر و هر نقطه برش ممکن، معیار Gini Impurity محاسبه می‌شود
    • Gini = 1 - Σ(p_i)²
      • p_i: نسبت کلاس i در گره
      • Gini=0: گره خالص (همه یک کلاس)
      • Gini=0.5: گره کاملاً ناخالص (50-50)
      </li>
      <li>تقسیمی که بیشترین کاهش Gini را دارد → انتخاب می‌شود</li>
  3. بازگشتی: فرآیند برای هر گره فرزند تکرار می‌شود
  4. توقف: وقتی:
    • گره خالص شود (همه یک کلاس)
    • تعداد نمونه کمتر از حداقل باشد (minsplit)
    • عمق به حداکثر برسد

مثال محاسبه Gini:

  • فرض کنید گره‌ای دارای 100 نمونه: 70 سالم، 30 بیمار
    • p_1 = 70/100 = 0.7
    • p_2 = 30/100 = 0.3
    • Gini = 1 - (0.7² + 0.3²) = 1 - 0.58 = 0.42
  • اگر بتوانیم آن را تقسیم کنیم به:
    • گره چپ: 20 سالم، 25 بیمار → Gini=0.49
    • گره راست: 50 سالم، 5 بیمار → Gini=0.17
    • میانگین وزنی: (45×0.49 + 55×0.17)/100 = 0.31
    • کاهش Gini: 0.42 - 0.31 = 0.11 (خوب!)

هرس کردن (Pruning):

  • درخت کامل معمولاً Overfit می‌کند
  • پارامتر cp (Complexity Parameter):
    • cp کوچک → درخت پیچیده‌تر
    • cp بزرگ → درخت ساده‌تر
  • caret بهترین cp را با Cross-Validation پیدا می‌کند

مزایا و معایب:

  • ✅ مزایا:
    • تفسیرپذیری کامل (می‌توان به پزشک توضیح داد)
    • نیاز به پیش‌پردازش کم (نرمال‌سازی لازم نیست)
    • مدیریت خودکار تعاملات غیرخطی
    • مقاوم در برابر داده‌های پرت
  • ❌ معایب:
    • ناپایدار (تغییر کوچک در داده → درخت کاملاً متفاوت)
    • تمایل به Overfitting
    • مرز تصمیم محدود به خطوط عمود بر محورها

6.4 مدل ۳: جنگل تصادفی (Random Forest)

set.seed(123)

rf_fit <- randomForest(
  ThryroidClass ~ .,
  data = train_scaled,
  ntree = 200,                    # 200 درخت
  importance = TRUE,              # محاسبه اهمیت متغیرها
  strata = train_scaled$ThryroidClass,  # نمونه‌برداری طبقه‌ای
  sampsize = rep(min(table(train_scaled$ThryroidClass)), 2)  # تعادل کلاس‌ها
)

print(rf_fit)
## 
## Call:
##  randomForest(formula = ThryroidClass ~ ., data = train_scaled,      ntree = 200, importance = TRUE, strata = train_scaled$ThryroidClass,      sampsize = rep(min(table(train_scaled$ThryroidClass)), 2)) 
##                Type of random forest: classification
##                      Number of trees: 200
## No. of variables tried at each split: 5
## 
##         OOB estimate of  error rate: 4.05%
## Confusion matrix:
##                     X.0.253900976999973 X3.93705202460583 class.error
## X.0.253900976999973                2392                89  0.03587263
## X3.93705202460583                    18               142  0.11250000
varImpPlot(rf_fit, main = "اهمیت متغیرها در جنگل تصادفی")

نتایج جنگل تصادفی:

  • نرخ خطای OOB (Out-of-Bag): 4.05% → دقت ≈ 96%
  • حساسیت (Sensitivity): حدود 89%
  • ویژگی (Specificity): حدود 97%

متغیرهای مهم (به ترتیب اولویت):

  1. T3_reading - بیشترین اهمیت
  2. TSH_reading - اهمیت بالا
  3. T4_reading - اهمیت متوسط
  4. FTI_reading - اهمیت متوسط
  5. سایر متغیرها: اهمیت کم

تفسیر نمودار اهمیت:

  • Mean Decrease Accuracy: اگر این متغیر را بر هم بزنیم (Permutation)، دقت چقدر کاهش می‌یابد؟
  • Mean Decrease Gini: این متغیر چقدر به کاهش Gini در تمام درخت‌ها کمک کرده؟
  • هر دو معیار تأیید می‌کنند: چهار متغیر هورمونی کلیدی هستند

نتیجه‌گیری:

  • مدل قوی و پایدار
  • تعادل خوب بین Sensitivity و Specificity
  • ویژگی بالا (97%) → مثبت کاذب کم

نحوه کار الگوریتم Random Forest:

مفهوم Ensemble Learning:

  • به جای یک مدل قوی، از “جمعیت” مدل‌های ضعیف استفاده می‌کنیم
  • قانون: “حکمت جمعی بهتر از فرد است”
  • مثال: 100 پزشک برای تشخیص رأی می‌دهند → تصمیم قوی‌تر

مراحل ساخت Random Forest:

  1. Bootstrap Sampling: برای هر درخت (مثلاً 200 درخت)
    • یک نمونه Bootstrap از داده‌های آموزش بردارید (نمونه‌گیری با جایگذاری)
    • حدود 63% داده‌ها انتخاب می‌شوند، 37% باقی می‌مانند (OOB)
  2. Feature Randomness: در هر گره از هر درخت
    • به جای بررسی تمام متغیرها، فقط زیرمجموعه تصادفی (معمولاً √p) بررسی می‌شود
    • مثال: اگر 30 متغیر داریم، در هر گره فقط √30 ≈ 5 متغیر بررسی می‌شوند
    • این باعث تنوع بین درخت‌ها می‌شود (Decorrelation)
  3. ساخت درخت: هر درخت روی داده Bootstrap خودش کاملاً رشد می‌کند (بدون هرس)
  4. Aggregation (رأی‌گیری):
    • طبقه‌بندی: رأی اکثریت (Majority Vote)
    • رگرسیون: میانگین پیش‌بینی‌ها

OOB Error (Out-of-Bag):

  • برای هر نمونه، درخت‌هایی که آن را ندیده‌اند (37% باقی‌مانده) روی آن تست می‌شوند
  • این یک ارزیابی رایگان است (بدون نیاز به داده تست جداگانه)
  • تخمین نزدیک به Cross-Validation

چرا Random Forest بهتر از درخت تصمیم تک است؟

  • کاهش Overfitting:
    • درخت تک: واریانس بالا، ناپایدار
    • RF: میانگین‌گیری → کاهش واریانس
  • مقاومت در برابر نویز:
    • اگر چند درخت اشتباه کنند، اکثریت جبران می‌کند
  • پایداری:
    • تغییر کوچک در داده → تغییر جزئی در عملکرد

پارامترهای کلیدی:

  • ntree: تعداد درخت‌ها (200-500 معمولاً کافی است)
  • mtry: تعداد متغیرهای بررسی‌شده در هر گره (پیش‌فرض: √p)
  • max_depth: حداکثر عمق درخت‌ها
  • min_samples_split: حداقل نمونه برای تقسیم

مزایا و معایب:

  • ✅ مزایا:
    • دقت بسیار بالا (معمولاً بهترین)
    • مقاوم در برابر Overfitting
    • محاسبه اهمیت متغیرها
    • نیاز کم به تنظیم فراپارامترها
    • OOB Error برای ارزیابی رایگان
  • ❌ معایب:
    • کمتر تفسیرپذیر (Black-box)
    • کند در پیش‌بینی (باید از همه درخت‌ها عبور کند)
    • حجم مدل بزرگ (ذخیره‌سازی)

6.5 مدل ۴: شبکه عصبی (Neural Network)

set.seed(123)

train_scaled$ThryroidClass <- as.factor(train_scaled$ThryroidClass)

nn_fit <- train(
  ThryroidClass ~ .,
  data = train_scaled,
  method = "nnet",
  trControl = ctrl,
  metric = "ROC",
  tuneLength = 5,          # آزمایش 5 ترکیب مختلف
  trace = FALSE            # عدم نمایش خروجی‌های میانی
)

print(nn_fit)
## Neural Network 
## 
## 2641 samples
##   31 predictor
##    2 classes: 'X.0.253900976999973', 'X3.93705202460583' 
## 
## No pre-processing
## Resampling: Cross-Validated (10 fold) 
## Summary of sample sizes: 2376, 2377, 2377, 2377, 2377, 2377, ... 
## Addtional sampling using up-sampling
## 
## Resampling results across tuning parameters:
## 
##   size  decay  ROC        Sens       Spec   
##   1     0e+00  0.8617393  0.8226584  0.84375
##   1     1e-04  0.8719241  0.8286987  0.85625
##   1     1e-03  0.8818816  0.8464309  0.83750
##   1     1e-02  0.8884843  0.8303083  0.86250
##   1     1e-01  0.9146396  0.8823245  0.83125
##   3     0e+00  0.9342339  0.9234114  0.80000
##   3     1e-04  0.8906728  0.9246211  0.75000
##   3     1e-03  0.9147312  0.9085115  0.76875
##   3     1e-02  0.9112299  0.9069261  0.81875
##   3     1e-01  0.9373114  0.9157922  0.80000
##   5     0e+00  0.9051054  0.9496227  0.74375
##   5     1e-04  0.9144806  0.9439775  0.73750
##   5     1e-03  0.9061536  0.9564775  0.72500
##   5     1e-02  0.9123274  0.9488227  0.72500
##   5     1e-01  0.9250364  0.9399598  0.76875
##   7     0e+00  0.8992082  0.9540565  0.72500
##   7     1e-04  0.9253039  0.9548695  0.76250
##   7     1e-03  0.9183645  0.9576904  0.73125
##   7     1e-02  0.9185217  0.9524453  0.73125
##   7     1e-01  0.9242505  0.9552662  0.77500
##   9     0e+00  0.9188203  0.9617389  0.65625
##   9     1e-04  0.9048643  0.9556824  0.67500
##   9     1e-03  0.8934085  0.9604984  0.70625
##   9     1e-02  0.9239882  0.9560808  0.72500
##   9     1e-01  0.9292169  0.9528469  0.73750
## 
## ROC was used to select the optimal model using the largest value.
## The final values used for the model were size = 3 and decay = 0.1.

نتایج شبکه عصبی:

  • بهترین ساختار: 5 نورون در لایه پنهان
  • Weight Decay: پارامتر Regularization برای جلوگیری از Overfitting
  • دقت کلی: حدود 94-95%
  • حساسیت: حدود 73-75% (پایین‌تر از سایر مدل‌ها)
  • ویژگی: حدود 96-97%

تفسیر:

  • دقت کلی خوب اما حساسیت پایین → خطر از دست دادن بیماران
  • برای کاربرد پزشکی، این مدل کمتر مناسب است
  • احتمالاً به دلیل:
    • عدم تعادل شدید داده‌ها
    • نیاز به تنظیم بیشتر فراپارامترها
    • احتمال گیر کردن در Local Minimum

نحوه کار شبکه عصبی:

ساختار شبکه Feed-forward:

  • لایه ورودی: تعداد نورون = تعداد ویژگی‌ها (مثلاً 30)
  • لایه پنهان: تعداد دلخواه نورون (مثلاً 5)
    • هر نورون: z = Σ(w_i × x_i) + b
    • تابع فعال‌سازی: a = sigmoid(z) = 1/(1+e^(-z))
  • لایه خروجی: 2 نورون (برای 2 کلاس)
    • Softmax برای تبدیل به احتمال

الگوریتم آموزش (Backpropagation):

  1. Forward Pass: داده از ورودی به خروجی حرکت می‌کند
  2. محاسبه خطا (Loss): Cross-Entropy Loss
    • L = -Σ y_i log(ŷ_i)
  3. Backward Pass: گرادیان خطا از خروجی به ورودی برگشت می‌کند
    • استفاده از Chain Rule
  4. به‌روزرسانی وزن‌ها: w_new = w_old - α × ∂L/∂w
    • α: Learning Rate (نرخ یادگیری)
  5. تکرار تا همگرایی

چالش‌ها:

  • Local Minima: ممکن است به جای بهترین جواب، در یک نقطه محلی گیر کند
  • تنظیم فراپارامترها:
    • تعداد لایه‌ها و نورون‌ها
    • Learning Rate
    • Weight Decay (Regularization)
    • تعداد Epochs
  • حساسیت به مقیاس‌بندی: نیاز حیاتی به نرمال‌سازی
  • عدم تعادل کلاس: شبکه به سمت کلاس اکثریت سوگیری پیدا می‌کند

چرا در اینجا عملکرد ضعیف‌تر است؟

  • داده نسبتاً ساده (روابط خطی و درختی کافی هستند)
  • شبکه‌های عصبی برای مسائل بسیار پیچیده (تصویر، متن) طراحی شده‌اند
  • برای دیتاست کوچک (3000 نمونه)، Overfitting محتمل‌تر است

7 بخش هفتم: مقایسه جامع مدل‌ها

fix_levels_safe <- function(x) {
  nums <- as.numeric(x)
  nums <- round(nums)
  nums[nums < 1] <- 1
  nums[nums > 2] <- 2
  factor(nums, levels = c(1, 2), labels = c("Negative", "Sick"))
}

actual_clean <- fix_levels_safe(test_scaled$ThryroidClass)

raw_knn <- predict(knn_fit, test_scaled, type = "raw")
p_knn_final <- fix_levels_safe(raw_knn)

raw_tree <- predict(tree_fit, test_scaled, type = "raw")
p_tree_final <- fix_levels_safe(raw_tree)

raw_rf <- predict(rf_fit, test_scaled, type = "response")
p_rf_final <- fix_levels_safe(raw_rf)

raw_nn <- tryCatch({
  predict(nn_fit, test_scaled, type = "raw")
}, error = function(e) {
  predict(nn_fit, test_scaled, type = "response")
})
p_nn_final <- fix_levels_safe(raw_nn)

eval_model <- function(pred, actual, model_name) {
  cm <- confusionMatrix(pred, actual, mode = "everything", positive = "Sick")
  
  data.frame(
    Model = model_name,
    Accuracy = round(cm$overall['Accuracy'], 4),
    Sensitivity = round(cm$byClass['Sensitivity'], 4),
    Specificity = round(cm$byClass['Specificity'], 4),
    F1_Score = round(cm$byClass['F1'], 4)
  )
}

results <- rbind(
  eval_model(p_knn_final,  actual_clean, "KNN"),
  eval_model(p_tree_final, actual_clean, "Decision Tree"),
  eval_model(p_rf_final,   actual_clean, "Random Forest"),
  eval_model(p_nn_final,   actual_clean, "Neural Network")
)

knitr::kable(results, caption = "جدول نهایی مقایسه عملکرد مدل‌ها")
جدول نهایی مقایسه عملکرد مدل‌ها
Model Accuracy Sensitivity Specificity F1_Score
Accuracy KNN 0.8691 0.8451 0.8708 0.4478
Accuracy1 Decision Tree 0.9390 0.9437 0.9387 0.6601
Accuracy2 Random Forest 0.9646 0.9718 0.9642 0.7753
Accuracy3 Neural Network 0.8939 0.8592 0.8962 0.5041

7.1 نمودار مقایسه

library(reshape2)
## 
## Attaching package: 'reshape2'
## The following object is masked from 'package:tidyr':
## 
##     smiths
results_long <- melt(results, id.vars = "Model")

ggplot(results_long, aes(x = Model, y = value, fill = variable)) +
  geom_bar(stat = "identity", position = "dodge", width = 0.7) +
  labs(title = "مقایسه جامع عملکرد مدل‌ها",
       x = "مدل",
       y = "نمره",
       fill = "معیار ارزیابی") +
  theme_minimal(base_size = 12) +
  theme(axis.text.x = element_text(angle = 45, hjust = 1, size = 11),
        legend.position = "top") +
  scale_fill_brewer(palette = "Set1") +
  geom_hline(yintercept = 0.9, linetype = "dashed", color = "red", alpha = 0.5) +
  annotate("text", x = 4.5, y = 0.92, label = "آستانه قبولی (90%)", 
           color = "red", size = 3)

7.2 ماتریس درهم‌ریختگی جنگل تصادفی

cm_rf <- confusionMatrix(p_rf_final, actual_clean, positive = "Sick")
print(cm_rf)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Negative Sick
##   Negative     1022    2
##   Sick           38   69
##                                           
##                Accuracy : 0.9646          
##                  95% CI : (0.9521, 0.9746)
##     No Information Rate : 0.9372          
##     P-Value [Acc > NIR] : 2.793e-05       
##                                           
##                   Kappa : 0.7569          
##                                           
##  Mcnemar's Test P-Value : 3.130e-08       
##                                           
##             Sensitivity : 0.97183         
##             Specificity : 0.96415         
##          Pos Pred Value : 0.64486         
##          Neg Pred Value : 0.99805         
##              Prevalence : 0.06278         
##          Detection Rate : 0.06101         
##    Detection Prevalence : 0.09461         
##       Balanced Accuracy : 0.96799         
##                                           
##        'Positive' Class : Sick            
## 

جدول خلاصه نتایج:

مدل دقت حساسیت ویژگی F1-Score رتبه
جنگل تصادفی 96.5% 97.2% 96.3% 0.7750 🥇 اول
درخت تصمیم 93.9% 94.4% 93.6% 0.6600 🥈 دوم
شبکه عصبی 94.9% 73.2% 96.3% 0.6420 🥉 سوم
KNN 86.9% 84.5% 87.2% 0.4480 چهارم

تفسیر ماتریس درهم‌ریختگی (Random Forest):

             Reference
Prediction   Negative  Sick
         Negative  1022     2
         Sick        38    69
  • True Negative (TN): 1022 - افراد سالم که صحیح تشخیص داده شدند ✅
  • False Positive (FP): 38 - افراد سالم که اشتباهاً بیمار تشخیص داده شدند ⚠️
    • عواقب: نگرانی بیهوده، آزمایش‌های اضافی، هزینه مالی
  • False Negative (FN): 2 - بیماران واقعی که مدل ندید ❌
    • عواقب: بسیار خطرناک! تأخیر در درمان، پیشرفت بیماری
    • این مهم‌ترین خطا در پزشکی است
  • True Positive (TP): 69 - بیماران که صحیح شناسایی شدند ✅✅

محاسبه معیارها:

  • حساسیت (Sensitivity / Recall): TP / (TP + FN) = 69 / 71 = 97.2%
    • معنی: از هر 100 بیمار، 97 نفر را می‌بینیم
    • برای پزشکی، این مهم‌ترین معیار است
  • ویژگی (Specificity): TN / (TN + FP) = 1022 / 1060 = 96.3%
    • معنی: از هر 100 سالم، 96 نفر را صحیح تشخیص می‌دهیم
  • دقت مثبت (Precision): TP / (TP + FP) = 69 / 107 = 64.5%
    • معنی: از هر 100 تشخیص “بیمار”، 65 نفر واقعاً بیمار هستند
  • F1-Score: 2 × (Precision × Recall) / (Precision + Recall) = 0.775
    • میانگین هارمونیک Precision و Recall
    • معیار متعادل برای عدم تعادل

8 نتیجه‌گیری نهایی و توصیه‌ها

🏆 مدل برنده: جنگل تصادفی (Random Forest)

دلایل انتخاب به عنوان مدل نهایی:

  1. بالاترین F1-Score (0.7750): تعادل بهینه بین دقت و بازیابی
  2. حساسیت فوق‌العاده (97.2%): تنها 2 بیمار از 71 نفر را از دست داد
    • این برای سیستم تشخیص پزشکی حیاتی است
  3. ویژگی بالا (96.3%): مثبت کاذب کم (38 از 1060)
  4. پایداری و اعتبار:
    • OOB Error تنها 4.05%
    • عملکرد یکنواخت در Cross-Validation
  5. شناسایی ویژگی‌های کلیدی: T3, TSH, T4, FTI به ترتیب اولویت

مقایسه با سایر مدل‌ها:

  • درخت تصمیم:
    • ✅ مزیت: تفسیرپذیری کامل
    • ❌ معایب: حساسیت کمتر، F1-Score پایین‌تر، ناپایدار
    • نتیجه: می‌تواند برای توضیح به پزشکان استفاده شود
  • شبکه عصبی:
    • ✅ مزیت: دقت کلی خوب (94.9%)
    • ❌ معایب: حساسیت بسیار پایین (73.2%) - خطرناک!
    • نتیجه: برای این مسئله نامناسب
  • KNN:
    • ❌ ضعیف‌ترین عملکرد در همه معیارها
    • نتیجه: رد می‌شود

یافته‌های علمی و پزشکی:

  • متغیرهای تشخیصی کلیدی:
    1. TSH (Thyroid Stimulating Hormone): قوی‌ترین نشانگر
      • افزایش چشمگیر در بیماران: 4.46 → 34.18 µU/mL
      • حلقه بازخورد منفی هیپوتالاموس-هیپوفیز-تیروئید
      </li>
      <li><strong>T3 (Triiodothyronine):</strong> شاخص فعالیت متابولیک</li>
      <li><strong>T4 و FTI:</strong> نشانگرهای تکمیلی</li>
  • متغیرهای کم‌اهمیت:
    • سن: همبستگی ضعیف
    • جنسیت: تأثیر حاشیه‌ای
  • تأیید دانش پزشکی: یافته‌های مدل کاملاً با فیزیولوژی شناخته‌شده مطابقت دارد

توصیه‌های کاربردی برای پیاده‌سازی:

  1. استقرار مدل:
    • استفاده از Random Forest به عنوان مدل اصلی
    • ذخیره مدل: saveRDS(rf_fit, “thyroid_model.rds”)
    • لود مدل در محیط تولید: model <- readRDS(“thyroid_model.rds”)
  2. آستانه تصمیم:
    • پیش‌فرض: 0.5 (احتمال > 50% → بیمار)
    • برای افزایش حساسیت: کاهش آستانه به 0.3
      • شناسایی بیشتر بیماران (کاهش FN)
      • افزایش مثبت کاذب قابل قبول
      </li>
  3. رابط کاربری:
    • ورودی: TSH, T3, T4, FTI (حداقل)
    • خروجی:
      • احتمال بیماری (0-100%)
      • تشخیص: سالم / مشکوک / بیمار
      • سطح اطمینان
      </li>
  4. یکپارچه‌سازی با سیستم بیمارستان:
    • اتصال به سیستم LIS (Laboratory Information System)
    • دریافت خودکار نتایج آزمایش
    • ارائه هشدار به پزشک برای موارد مشکوک

مسیرهای بهبود در آینده:

  • ۱. جمع‌آوری داده‌های بیشتر:
    • افزایش نمونه‌های کلاس اقلیت (بیماران)
    • تنوع جمعیت‌شناختی (سن، نژاد، جنسیت)
  • ۲. ویژگی‌های جدید:
    • آنتی‌بادی‌های تیروئید (Anti-TPO, Anti-TG)
    • تصویربرداری (سونوگرافی تیروئید)
    • علائم بالینی (خستگی، تغییر وزن، ضربان قلب)
    • سابقه خانوادگی
  • ۳. تشخیص چندکلاسه:
    • تفکیک انواع بیماری: هیپوتیروئید، هیپرتیروئید، گواتر، تیروئیدیت
  • ۴. مدل‌های پیشرفته‌تر:
    • Gradient Boosting (XGBoost, LightGBM)
    • Stacking Ensemble (ترکیب چند مدل)
    • Deep Learning (برای دیتاست بزرگ‌تر)
  • ۵. تحلیل خطاها:
    • بررسی دقیق 2 مورد False Negative
    • یافتن الگوی مشترک موارد اشتباه
    • اصلاح مدل بر اساس یافته‌ها

ملاحظات اخلاقی و قانونی:

  • ⚠️ این مدل یک ابزار کمک تشخیصی است، نه جایگزین پزشک
  • تصمیم نهایی همیشه با پزشک متخصص است
  • رضایت آگاهانه بیمار برای استفاده از سیستم AI
  • حفظ حریم خصوصی داده‌های پزشکی (HIPAA, GDPR)
  • مسئولیت‌پذیری در مقابل خطاهای مدل